#!/usr/bin/python
#
# Copyright (c) 2009 Julius Volz
# See LICENSE for details.

import conv, debug, rdfpath, re, silk, stemmer, unicodedata


class Error(silk.Error):
  """Used to signal errors during function evaluation."""


class Param(object):
  def evaluate(self, bindings, trace=None):
    pass


class LiteralParam(Param):
  def __init__(self, value):
    self.value = value

  def evaluate(self, bindings, trace=None):
    return [{'type': 'literal', 'value': self.value}]


class RDFPathParam(Param):
  def __init__(self, path):
    self.path = rdfpath.RDFPath(path)

  def evaluate(self, bindings, trace=None):
    if trace is not None:
      trace['path'] = self.path
    return self.path.evaluate(bindings)

  def __str__(self):
    return self.path.path


class FunctionParam(Param):
  def __init__(self, func_call):
    self.func_call = func_call

  def evaluate(self, bindings, trace=None):
    return self.func_call.evaluate(bindings, trace)


functions = {}

def get(identifier):
  if identifier not in functions:
    raise Error('Function "%s" not defined!' % identifier)
  return functions[identifier]

def register(*args):
  for func in args:
    if not issubclass(type(func), Function):
      raise Error('Trying to register non-function "%s" as function!' % func)

    if func.identifier in functions:
      raise Error('Function "%s" registered twice!' % func.identifier)
    functions[func.identifier] = func

def unregister(identifier):
  del functions[identifier]


class Function(object):
  param_types = {}
  identifier = None

  def check_params(self, params):
    """Check whether all required parameters are present."""
    for param in self.param_types:
      if param not in params:
        raise Error('Parameter "%s" missing in call to function "%s"!' %
                    (param, self.identifier))

  def evaluate(self, **kwargs):
    """Actual function implementation."""


class FunctionCall(object):
  def __init__(self, func, params):
    func.check_params(params)
    self.func = func
    self.params = params

  def evaluate_params(self, bindings, trace=None):
    """Bind parameters."""
    param_values = {}
    for param_name in self.func.param_types:

      param_trace = None
      if trace is not None:
        param_trace = {
          'obj': self.params[param_name],
          'name': param_name
        }
        trace.append(param_trace)

      # Evaluate the set of values of a parameter
      values = self.params[param_name].evaluate(bindings, param_trace)

      if trace is not None:
        param_trace['value'] = values

      if values is None:
        debug.log(5, 'Parameter "%s" evaluates to "None", returning "None".\n'
                  % param_name)
        return None
      # TODO: is this always wanted?
      if len(values) == 0:
        debug.log(5, 'Parameter "%s" (%s) evaluates to empty list, returning "None".\n'
                  % (param_name, self.params[param_name]))
        return None

      # After evaluating, cast the parameter
      try:
        ptype = self.func.param_types[param_name]
        if type(ptype) == list:
          values = [ptype[0](value) for value in values]
        else:
          values = ptype(values[0])
        param_values[param_name] = values
      # Some type cast failed, TODO: recover from some cases
      except ValueError:
        debug.log(5, 'Failed parameter cast, returning "None".\n')
        return None

    return param_values

  def call_func(self, param_values):
    return self.func.evaluate(**param_values)

  def evaluate(self, bindings, trace=None):
    """Bind parameters and evaluate the function."""
    param_trace = None
    if trace is not None:
      trace.update({
        'obj': self.func,
        'params': []
      })
      param_trace = trace['params']

    param_values = self.evaluate_params(bindings, param_trace)

    if trace is not None:
      trace['value'] = param_values

    if param_values is None:
      return None

    return self.call_func(param_values)


class LowerCase(Function):
  identifier = 'lowerCase'
  param_types = { 'string': [conv.raw] }

  def evaluate(self, string):
    result = [x.copy() for x in string]
    for r in result:
      r['value'] = r['value'].lower()

    return result
  
  
class UpperCase(Function):
  identifier = 'upperCase'
  param_types = { 'string': [conv.raw] }

  def evaluate(self, string):
    result = [x.copy() for x in string]
    for r in result:
      r['value'] = r['value'].upper()

    return result


class Concat(Function):
  identifier = 'concat'
  param_types = {
    'str1': [conv.raw],
    'str2': conv.string
  }

  def evaluate(self, str1, str2):
    result = [x.copy() for x in str1]
    for r in result:
      r['value'] += str2

    return result


class Replace(Function):
  identifier = 'replace'
  param_types = {
    'string': [conv.raw],
    'search': conv.string,
    'replace': conv.string
  }

  def evaluate(self, string, search, replace):
    result = [x.copy() for x in string]
    for r in result:
      r['value'] = r['value'].replace(search, replace)

    return result


class RegexReplace(Function):
  identifier = 'regexReplace'
  param_types = {
    'string': [conv.raw],
    'regex': conv.string,
    'replace': conv.string
  }

  def evaluate(self, string, regex, replace):
    p = re.compile(regex)

    result = [x.copy() for x in string]
    for r in result:
      r['value'] = p.sub(replace, r['value'])

    return result


class SpecialRegexReplace(Function):
  param_types = { 'string': [conv.raw] }
  p = None

  def evaluate(self, string):
    result = [x.copy() for x in string]
    for r in result:
      r['value'] = self.p.sub('', r['value'])

    return result


class AlphaReduce(SpecialRegexReplace):
  identifier = 'alphaReduce'
  p = re.compile('[^a-zA-Z]+') # TODO: other alnum characters


class NumReduce(SpecialRegexReplace):
  identifier = 'numReduce'
  p = re.compile('[0-9]+')


class RemoveBlanks(SpecialRegexReplace):
  identifier = 'removeBlanks'
  p = re.compile('\s+')


class RemoveSpecialChars(Function):
  identifier = 'removeSpecialChars'
  param_types = { 'string': [conv.raw] }
  p = re.compile('[^a-zA-Z]+')

  def evaluate(self, string):
    result = [x.copy() for x in string]
    for r in result:
      value = unicodedata.normalize('NFKD', r['value'])
      value = value.encode('ASCII','ignore')
      r['value'] = self.p.sub(' ', value)

    return result


class Stem(Function):
  identifier = 'stem'
  param_types = { 'string': [conv.raw] }
  word_stemmer = stemmer.PorterStemmer()

  def evaluate(self, string):
    result = [x.copy() for x in string]
    for r in result:
      r['value'] = self.word_stemmer.stem(r['value'], 0, len(r['value']) - 1)

    return result


class StripURIPrefix(Function):
  identifier = 'stripURIPrefix'
  param_types = { 'string': [conv.raw] }

  def evaluate(self, string):
    result = [x.copy() for x in string]
    for r in result:
      v = r['value']
      r['value'] = v[max(v.rfind('/'), v.rfind('#')) + 1:]

    return result


class TranslateWithDictionary(Function):
  identifier = 'translateWithDictionary'
  param_types = {
    'string': [conv.raw],
    'file': conv.string
  }

  dicts = {}

  def evaluate(self, string, dict_file):
    dictionary = self.get_dict(dict_file)

    result = [x.copy() for x in string]
    for r in result:
      r['value'] = dictionary.get(r['value'], r['value'])

    return result

  def get_dict(self, dict_file):
    if not dict_file in self.dicts:
      self.dicts[dict_file] = {}
      f = open(dict_file)
      for line in f.readlines():
        word, translation = line.strip().split(':')
        self.dicts[dict_file][word] = translation
        
      f.close()

    return self.dicts[dict_file]


register(
  LowerCase(),
  UpperCase(),
  Concat(),
  Replace(),
  RemoveBlanks(),
  RemoveSpecialChars(),
  RegexReplace(),
  NumReduce(),
  AlphaReduce(),
  Stem(),
  StripURIPrefix(),
  TranslateWithDictionary()
)
